package web.suzy.oj.shiro;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import web.suzy.oj.annotation.AnonApi;
import web.suzy.oj.common.result.ResultBean;
import web.suzy.oj.common.result.ResultStatus;
import web.suzy.oj.utils.JwtUtils;
import web.suzy.oj.utils.RedisUtils;
import web.suzy.oj.utils.ServiceContextUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * YangSuzy 软件工作室
 * 类名: JwtFilter
 * 描述: ---- 待定 -----
 * 功能: ---- 待定 -----
 *
 * @author YangSuzy
 * Date: 2022/11/14 16:31
 */
@Component
@Slf4j(topic = "suzyoj")
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private RedisUtils redisUtils;

    /**
     * 方法名: preHandle
     * 描述: controller方法调用前执行，对跨域提供支持
     *
     * @param request
     * @param response
     * @return boolean
     * @date 2022/11/14 17:01
     * @auther YangSuzy
     **/
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        //接受任意域名的请求
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        //服务器支持的所有跨域请求的方法
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        //服务器支持的所有头信息字段
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //可以让前端访问列出的响应头字段信息
        httpServletResponse.setHeader("Access-Control-Expose-Headers",
                "Refresh-Token,Authorization,Url-Type,Content-disposition,Content-Type");
        //跨域时首先会发送一个OPTIONS请求，这里给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 方法名: isAccessAllowed
     * 描述: 判断请求是否通过，完成权限鉴定
     *
     * @param request     请求
     * @param response    响应
     * @param mappedValue 映射值
     * @return boolean
     * @date 2022/11/14 16:34
     * @auther YangSuzy
     **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //WebUtils -> 解决跨域问题
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        WebUtils.saveRequest(httpRequest);
        //获取应用请求上下文的处理程序
        WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpRequest);
        RequestMappingHandlerMapping mapping = ctx.getBean(
                "requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        try {
            //根据上下文获取请求的处理方法
            HandlerExecutionChain handler = mapping.getHandler(httpRequest);
            HandlerMethod handlerClazz = (HandlerMethod) handler.getHandler();
            //判断请求是否访问公共接口，如果拥有@AnonApi注解则不走登录认证，直接访问controller对应的方法
            AnonApi anonApi = ServiceContextUtils.getAnnotation(handlerClazz.getMethod(),
                    handlerClazz.getBeanType(),
                    AnonApi.class);
            return anonApi != null;
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 方法名: createToken
     * 描述: 获取认证凭证
     *
     * @param servletRequest  请求
     * @param servletResponse 响应
     * @return org.apache.shiro.authc.AuthenticationToken
     * @date 2022/11/14 16:37
     * @auther YangSuzy
     **/
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //获取 token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StrUtil.isBlank(jwt)) {
            return null;
        }
        return new JwtToken(jwt);
    }

    /**
     * 方法名: onAccessDenied
     * 描述: isAccessAllowed()请求未通过时执行此方法，处理权限鉴定失败情况
     *
     * @param servletRequest  请求
     * @param servletResponse 响应
     * @return boolean
     * @date 2022/11/14 16:39
     * @auther YangSuzy
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader("Authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        //判断凭证是否已过期
        Claims claim = jwtUtils.getClaimByToken(token);
        if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
            return this.onLoginFailure(null,
                    new AuthenticationException("登录状态已失效，请重新登录！"), servletRequest, servletResponse);
        }

        //根据凭证获取用户ID
        String userId = claim.getSubject();

        //如果校验(请求携带的token)与(redis缓存中token)，
        //会造成当前登录成功，之前登录存在的token失效。
        //对于OJ来说，允许多地方登录在线。
        boolean hasToken = jwtUtils.hasToken(userId);
        //检查缓存中是否存在token，不存在时需重新登录
        if (!hasToken) {
            return this.onLoginFailure(null,
                    new AuthenticationException("登录状态已失效，请重新登录！"), servletRequest, servletResponse);
        }
        //检查缓存中的token是否需要刷新
        if (!redisUtils.hasKey(ShiroConstant.SHIRO_TOKEN_REFRESH + userId)) {
            //过了需更新token时间，但是还未过期，则进行token刷新
            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
            HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
            this.refreshToken(httpRequest, httpResponse, userId);
        }
        // 执行自动登录
        return executeLogin(servletRequest, servletResponse);
    }

    /**
     * 方法名: refreshToken
     * 描述: 刷新 token并更新到前端
     *
     * @param request  请求
     * @param response 响应
     * @param userId   用户ID
     * @return void
     * @date 2022/11/14 16:48
     * @auther YangSuzy
     **/
    private void refreshToken(HttpServletRequest request, HttpServletResponse response, String userId) throws IOException {
        //获取20s的分布式锁
        boolean lock = redisUtils.getLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId, 20);
        if (lock) {
            //创建新的token
            String newToken = jwtUtils.generateToken(userId);
            //浏览器可以将Cookie或其他资源包含在请求中发给服务器
            response.setHeader("Access-Control-Allow-Credentials", "true");
            //将新的token放到响应头
            response.setHeader("Authorization", newToken);
            //可以让前端访问列出的响应头字段信息
            response.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type");
            //为了前端能区别请求来源
            response.setHeader("Url-Type", request.getHeader("Url-Type"));
            //告知前端需要刷新token
            response.setHeader("Refresh-Token", "true");
        }
        //释放锁
        redisUtils.releaseLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId);
    }

    /**
     * 方法名: onLoginFailure
     * 描述: 登录失败时执行此方法
     *
     * @param token    认证凭证
     * @param e        认证异常
     * @param request  请求
     * @param response 响应
     * @return boolean
     * @date 2022/11/14 16:58
     * @auther YangSuzy
     **/
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        returnErrorResponse(request, response, e, ResultStatus.ACCESS_DENIED);
        return false;
    }

    /**
     * 方法名: returnErrorResponse
     * 描述: 登录失败时返回错误响应信息
     *
     * @param request      请求
     * @param response     响应
     * @param e            异常
     * @param resultStatus 结果状态
     * @return void
     * @date 2022/11/14 17:00
     * @auther YangSuzy
     **/
    private void returnErrorResponse(ServletRequest request, ServletResponse response, Exception e, ResultStatus resultStatus) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            ResultBean<Void> result = ResultBean.errorResponse(throwable.getMessage(), resultStatus);
            String json = JSONUtil.toJsonStr(result);
            //设置响应为Json类型
            httpResponse.setContentType("application/json;charset=utf-8");
            //可以让前端访问列出的响应头字段信息
            httpResponse.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type");
            //浏览器可以将Cookie或其他资源包含在请求中发给服务器
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            //为了前端能区别请求来源
            httpResponse.setHeader("Url-Type", httpRequest.getHeader("Url-Type"));
            //设置响应状态
            httpResponse.setStatus(resultStatus.getStatus());
            httpResponse.getWriter().print(json);
        } catch (IOException ignored) {
        }
    }
}
